/**
 * 会话服务器，保持长连接机制
 * ps：以事件的形式来保持长连接，同一个连接如不关闭，除了第一次会发一个connect，其他只要客户端同一个socket发送心跳
 *     服务器只触发data事件。
 */
var net = require('net');
var express = require('express'); 
var msgpack = require('./framedmsgpack');
var dateHelper = require('./helper/dateHelper');
var redisHelper = require('./helper/redisHelper'); 
var qqwry = null;
// qqwry = require('./helper/qqwry/index.js').info(); // 纯真ip库
redisHelper = new redisHelper();

// socket队列
var connections = [];
var THRESHOLD = 86400000; // TOKEN过期阀值

// 捕捉全局异常
process.on('uncaughtException', function(err) {
	console.log('uncaughtException: ' + err);
});

// 每个客户端socket连上服务器，服务器都会触发一个属于每个客户端自己的connect事件回调闭包
var server = net.createServer(function(connection) { // connection是一个socket实例，实现了readable和writeable接口
	console.log('----------------------------------------------');
	console.log('\nsocket.----------------------------------------------> ' + dateHelper.getNowFormatTime());
	var ip = connection.remoteAddress;
	if(qqwry) { // 开启
		ip = qqwry.searchIP(ip); // 解析ip地址 eg：{ ip: '115.28.1.119', Country: '北京市', Area: '万网IDC机房' }
	} 
	console.log('Connect：socket connected：%s %s %s', 
		ip.ip === undefined ? ip : ip.ip, 
		ip.Country === undefined ? '' : ip.Country, 
		ip.Area === undefined ? '' : ip.Area);

	var auth = false; 
	var index = 0; // socket队列下标
	var userInfo = null; // 一个用户一条连接：客户端传消息Object原型对象userInfo {'userName':'...', 'pushToken':'...',deviceId:'',....}
	// 缓存机制
	var buffers = [];
	var loginTime = 0; // 登录时间
	var pastDue = false; // token过期

	// 心跳机制：7分钟未收到心跳促发关闭事件
	connection.setTimeout(420000, function() {
		connection.destroy(); // 关闭此次socket连接，会出发close事件
	});

	// 传输的字节流，在这里是字节流一节节的不能保证一次就是一组完整数据，先校验数据完整性
	connection.on('data', function(trunk) { // trunk是一个默认8k长度的Buffer
		// step1：authenticate
		if(!auth) {
			var msg = analyseWrapper(trunk, buffers);
			if(!msg.isWrap) // 不够一次完整信息，已缓存到buffer
				return;
			userInfo = msg.message; // json即Object对象原型

			if(userInfo === null) {
				connection.write(msgpack.packWithType({"result": false, "code": 403, "message": "please send {userName:'.', pushToken:'.', deviceId:'.'} by this way.", "timestamp": dateHelper.getNowFormatTime1()}, 1)); 
				connection.destroy();
				return;
			}
			// userInfo = JSON.stringify(userInfo); 转成unicode编码
			userInfo = eval("(" + userInfo + ")"); // 因为是一个字符串最外层有个""，先转换成一个标准的JSON对象
			
			console.log('pushToken\'s key：userName=%s, devceId=%s, _pushToken', userInfo.userName, userInfo.deviceId);
			console.log('pushToken\'s value: %s', userInfo.pushToken);

			// 验证设备是否有token签到？
			redisHelper.get(userInfo.userName + userInfo.deviceId + '_pushToken', function(error, reply) {
				if(error) { // 如果错了reply为undefined，没错没有数据即error=null、reply=null
					connection.write(msgpack.packWithType({"result": false, "code": 500, "message": "redis server exception.", 
						"timestamp": dateHelper.getNowFormatTime1()}, 1)); 
					connection.destroy();
					return;
				}
				if(reply === null || reply.split('@')[0] !== userInfo.pushToken) { // 验证是否通过鉴权
					connection.write(msgpack.packWithType({"result": false, "code": 403, "message": "please do sth before authenticate.", 
						"timestamp": dateHelper.getNowFormatTime1()}, 1)); 
					connection.destroy();
					return;
				}
				// 保存Token时间
				loginTime = Date.now();
				// 验证token是否过期
				if(loginTime - reply.split('@')[1].valueOf() > THRESHOLD) {
					pastDue = true;
					connection.write(msgpack.packWithType({"result": false, "code": 403, "message": "pushToken was pasted due, please authenticate again.", "timestamp": dateHelper.getNowFormatTime1()}, 1)); 
					connection.destroy();
					return;
				}
				auth = true; // 鉴权通过
				index = connections.push(connection) - 1; // Hash表维护socket队列
				console.log(connections.length + ', ' + connections[index].valueOf());
				/**
				 * 缓存socket会话队列；是否需要用户？同一台设备上切换用户后pushToken丢失！！每次都要鉴权
				 */
				redisHelper.set(/*userInfo.userName + */userInfo.deviceId + '_sockets', index, function(error, reply, index) { // 覆盖之前
					if(error) {
						console.log('redis set error [socket]...' + error); 
						connection.write(msgpack.packWithType({"result": false, "code": 500, "message": " redis on server exception.", 
							"timestamp": dateHelper.getNowFormatTime1()}, 1));
						// 释放无效socket
						connection.destroy();
					} else {
						console.log('redis save [socket]...' + reply); 

						if(connections[index]) // 复查消息队列
							connections[index] = connection;

						connection.write(msgpack.packWithType({"result": true, "code": 200, "message": "device check in yet.", 
							"timestamp": dateHelper.getNowFormatTime1()}, 1));
					}
				});
				/** 离线消息 zilla不需要，因为有Push-Server负责发送离线*/
			});
		} else {
			// step2: Token过期机制
			if(Date.now() - loginTime > THRESHOLD) {
				pastDue = true;
				connection.write(msgpack.packWithType({"result": false, "code": 403, "message": "pushToken was pasted due, please authenticate again.", "timestamp": dateHelper.getNowFormatTime1()}, 1)); 
				connection.destroy();
				return;
			}
			// step3: 接收信息，心跳
			var message = analyseWrapper(trunk, buffers);
			if (message.isWrap && message.message == '') { // js里面的''表示一个字节,可能其他语言的" "表示一个字节
				connection.write(msgpack.packWithType('', 1)); // <Buffer 00 00 00 01 01 a0>
				console.log('heart router：4 min ...');
				return;
			}
			/**
			 * 接收消息回执 if((message.isWrap && message.message == 'xxxx') 
			 */
		} 
	});

	// 当一个错误发生时触发。 'close' 事件将直接被下列时间调用。
	connection.on('error', function(error) {
		redisHelper.delete(/*userInfo.userName +*/ userInfo.deviceId + '_sockets', function(error, reply) {
			// socket数组队列删除无效socket
			connections.splice(reply, 1); 

			console.log('delete [socket]=' + reply);
			console.log(connections.length + ', check obj isExsit=' + connections[reply]);
			console.log('%s %s %s \'s delete socket Error: -----> %s', 
			ip.ip === undefined ? ip : ip.ip, 
			ip.Country === undefined ? '' : ip.Country, 
			ip.Area === undefined ? '' : ip.Area, error);
		});
	});
	
	// server的socket端给客户端发送Fin数据包请求关闭
	connection.on('end', function() {
		// 1.同一台设备上更换用户 2.Token过期 3.应用程序退出
		// if(auth) {
		// 	redisHelper.delete(userInfo.userName + userInfo.deviceId + '_pushToken', function(error, reply) {
		// 		console.log('delete pushToken.' + reply);
		// 	});	
		// }
		console.log('%s %s %s --> Fin：socket has disconnected that beceuse of client send FIN dataPackage', 
			ip.ip === undefined ? ip : ip.ip, 
			ip.Country === undefined ? '' : ip.Country, 
			ip.Area === undefined ? '' : ip.Area);
	});

	// 当套接字完全关闭时该事件被分发。参数had_error是一个布尔值，表示了套接字是否因为一个传输错误而被关闭。
	connection.on('close', function(had_error) { // 客户端socket端向server发送Fin请求关闭，或者手工请求关闭
		console.log('%s %s %s --> Close：connection has closed is cause by error? -> %s', 
			ip.ip === undefined ? ip : ip.ip, 
			ip.Country === undefined ? '' : ip.Country, 
			ip.Area === undefined ? '' : ip.Area, had_error);

		// 鉴权服务器 删除此用户Token信息（token其实有个有效期）
		if(pastDue) {
			redisHelper.delete(userInfo.userName + userInfo.deviceId + '_pushToken', function(error, reply) {
				console.log('delete pushToken.' + reply);
			});	
		}
		// 会话服务器 删除此次会话
		if(auth && !had_error) {
			redisHelper.delete(/*userInfo.userName +*/ userInfo.deviceId + '_sockets', function(error, reply) {
				// socket数组队列删除无效socket
				connections.splice(reply, 1); 
				
				console.log('delete [socket]=' + reply);
				console.log(connections.length + ', check obj isExsit=' + connections[reply]);
				console.log('%s %s %s \'s delete socket Error: -----> %s', 
				ip.ip === undefined ? ip : ip.ip, 
				ip.Country === undefined ? '' : ip.Country, 
				ip.Area === undefined ? '' : ip.Area, error);
			});
		}
	});

	// setInterval(sayHello, 10000, connection); 不要这样写！
	/**
	 * 把耗时方法拆分，重新分配到事件队列的队尾，等待最后促发
	 *
	process.nextTick(function() {
		msgpack.getWholeWrap(trunk)
		sayHello(connection);
		sayImage(connection);
	}); */
});

/**
 * 根据固定格式分析完整包，如果是就直接返回，不是就缓存（多了就返回信息再缓存，少了直接缓存）
 */
function analyseWrapper(trunk, buffers) {
	// 假如有上次遗留信息
	if(buffers.length > 0) {
			var chunk = buffers.pop(); // 弹出队尾
			var trunks = new Buffer(chunk.length + trunk.length); // 重构buffer
			chunk.copy(trunks, 0);
			trunk.copy(trunks, chunk.length);
			buffers.splice(0, buffers.length);// 清空Buffer缓存数组
			trunk = trunks;
	}
    // 第一次连接，没有鉴权，但只发小于5的流，过滤
    if(trunk.length < 5) {
		return {isWrap: true, message: null};
	}  
	/** 
	 * 解析完整消息
	 */
	var message = msgpack.getWholeWrap(trunk);
	if (message.isWrap && message.buffer === null) { // 完整的信息
		console.log('it is Wrap.');
		// console.log(JSON.stringify(message.message) + '|' + message.type);
		// console.log(message.message + '|' + message.type);
	} else if(message.isWrap && message.buffer !== null) { // 一条完整信息包含下一条信息的部分内容
		console.log('it is Wrap but include next.');
		// console.log(message.message + '|' + message.type);
		// console.log(JSON.stringify(message.message) + '|' + message.type);
		buffers.push(message.buffer);
	} else if(!message.isWrap &&  message.buffer !== null) { // 不够一条完整信息
		console.log('it is not Wrap.');
		buffers.push(message.buffer);
	}
	return message;
}

/*function sayHello(connection) {
	console.log('sayHello, ' + new Date());

	var o = {"type" : "message", "from": "justin", "to": "what", "content": "haha", "timestamp": new Date()}; // new Object()
	var framedBuffer = msgpack.packWithType(o, 31);
	connection.write(framedBuffer);
	// connection.pipe(connection); 
}
function sayImage(connection) {
	console.log('sayHello, ' + new Date());

	var o = {"type" : "message", "from": "justin", "to": "what", "content": "haha", "timestamp": new Date()};
	var framedBuffer = msgpack.packWithType(o, 32);
	connection.write(framedBuffer);
}*/

// 当一个错误发生时触发。 'close' 事件将直接被下列时间调用。
server.on('error', function(error) {
	console.log('server error: ' + error);

	if (error.code == 'EADDRINUSE') {
		console.log('Address in use, retrying...');	
		/*server.listen(6878, function(){
			console.log('socket server bound again, port:6878.');
		});*/
	}
});

// 当服务被关闭时触发. 注意：如果当前仍有活动连接，他个事件将等到所有连接都结束后才触发。
server.on('close', function() {
	console.log('server closed....');
	// 自动重启，删除所有redis缓存Socket队列
	// server.listen(6868, function() {
	// 	console.log('socket server bound again, port:8868.');
	// });
});

//######## startup #################
require('./auth.js')();
// require('./dispacher.js')();

server.listen(/*process.env.PORT ||*/ 8868, function() {
    console.log('socket server bound, port:8868.');
});

/**
 * 推送数据数据API
 */
var messageDAO = require('./models/message'); // 持久化工具
var app = express();
app.use(express.bodyParser()); // 使用中间件

var APP_ID = require('./config/config').APP_ID;
var APP_SECRET = require('./config/config').APP_SECRET; 

app.post('/pushMsg', function(req, res) {
	console.log('--------------------------------------------------------');
	console.log('\n--------------------------------------------------------');
	var ip = req.ip;
	if(qqwry)
		ip = qqwry.searchIP(req.ip); // 解析ip地址 eg：{ ip: '115.28.1.119', Country: '北京市', Area: '万网IDC机房' }

	console.log('Request：request coming：%s, %s, %s', ip.ip === undefined ? ip : ip.ip, 
			ip.Country === undefined ? '' : ip.Country, 
			ip.Area === undefined ? '' : ip.Area);
	console.log('Request body:');
	console.log(req.body); // msg应该带有设备id

	// step1 获取msg
	var msg = req.body.msg;
	var deviceId = req.body.deviceId;

	if (APP_ID !== req.body.appId || APP_SECRET !== req.body.secret || deviceId === undefined || deviceId === '') {
		res.status(401).json({'message': 'request validation fail.'});
		return;
	}
	/**
	 * 根据设备发送消息
	 */
	redisHelper.get(deviceId + '_sockets', function(error, reply) {
		if(!error) {
			msg = eval("(" + msg + ")"); // 转换msg对象
			// msg: '{"id":"17d","title":"公告","messageType":"MODULE","extras":{"moduleIdentifer":"com.foss.announcement"}}', deviceId: '00000000-4df8-d963-3b64-93f651a1e20a'
			var message = {
				msgId: msg.id,
				msgType: msg.messageType,
				title: msg.title,
				target: deviceId,
			}
			if(msg.extras) { // 存在扩展信息情况，例如推送公告
				message.extras = msg.extras;
			}
			console.log('step1 开始保存日志：' + message.msgId);
			if (reply) { // 说明此socktet在长连接队列中 
				// mongo更新成功状态
				message.receive = true;
				messageDAO.save(message, function(err) {
					if (err) {
						console.log('插入message对象异常：' + err);
					}
					console.log('step2 开始发送消息：' + 'socket队列=' + reply + ' --> ' + deviceId);
					
					if(connections[reply.valueOf()]) {
						connections[reply.valueOf()].write(msgpack.packWithType(msg, 1)); // 推送给客户端APP
						res.status(200).json(200);		
					} else {
						console.log('无效队列，重新连接！');
						res.status(200).json(304);
					}
				});
			} else {
				console.log('此设备未保持长连接，不过没关系，登录长连接后它会去平台拉取离线消息！');
				// mongo更新失败状态
				message.receive = false;
				messageDAO.save(message, function(err) {
					if (err) {
						console.log('插入message对象异常：' + err);
					}
				});
				res.status(200).json(304);
			}
		} else { // redis连接器异常
			res.status(200).json(500); // eclipse
		}
	});
});

app.listen(8869);
console.log('Push API server bound, port:8869.');